Vue nextTick 官方文档 (opens new window):
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
nextTick 函数的核心逻辑就是利用宏任务和微任务机制。当前 DOM 更新为宏任务,在调用 nextTick 函数后,会将回调函数添加到回调函数列表(异步队列)中,并将异步队列的函数调用放到微任务(Promise.then、MutationObserver、setImmediate 或 setTimeout)中执行。
# nextTick 函数
var callbacks = []; // 异步队列
var pending = false; // 设置是否等待执行标志
function nextTick (cb, ctx) {
var _resolve;
// 将回调函数添加到 nextTick 回调函数列表中
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true; // 设置正在等待执行标志
timerFunc(); // 该函数会将回调函数列表的函数调用放到微任务中执行
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
函数执行逻辑:
- 将回调函数添加到异步队列中。
- 如果异步队列还未开始等待执行,则将 pending 设置为 true,表示等待执行,即等待在下次 DOM 更新循环结束之后执行延迟回调。
- 调用 timerFunc 函数,该函数会将回调函数列表的函数调用放到微任务中执行。
# timerFunc 函数
该函数会将回调函数列表的函数调用放到微任务(Promise.then、MutationObserver、setImmediate 或 setTimeout)中执行。
Vue 在内部对异步队列尝试使用原生的
Promise.then
、MutationObserver
和setImmediate
,如果执行环境不支持,则会采用setTimeout(fn, 0)
代替。
# Promise.then 实现
var timerFunc;
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
// 在有问题的 UIWebViews 中,Promise.then 并没有完全中断,但它可能会陷入一种奇怪的状态,回调被推入微任务队列但队列没有被刷新,直到浏览器需要做一些其他工作,例如 处理一个计时器。 因此,我们可以通过添加一个空计时器来“强制”刷新微任务队列。
if (isIOS) { setTimeout(noop); }
};
isUsingMicroTask = true;
# MutationObserver 实现
var counter = 1;
var observer = new MutationObserver(flushCallbacks); // 当文本节点变更后会执行回调 flushCallbacks
var textNode = document.createTextNode(String(counter)); // 创建文本节点
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2; // 变更节点内容
textNode.data = String(counter);
};
isUsingMicroTask = true;
MutationObserver (opens new window) 会创建并返回一个新的 MutationObserver 它会在指定的 DOM 发生变化时被调用。上面逻辑通过创建一个文本节点,当调用 timerFunc 函数的时候,则会手动触发文本节点内容的变更,从而触发 flushCallbacks 函数的执行。
# 时间函数实现
// setImmediate
timerFunc = function () {
setImmediate(flushCallbacks);
};
// setTimeout
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
# flushCallbacks 函数
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
上面函数的执行逻辑:
- 将异步队列等待状态 pending 设置为 false。
- 遍历异步队列,执行每一个 nextTick 回调函数。